import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.Socket;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Random;
import java.util.StringTokenizer;

import javax.swing.Timer;

/**
 * 
 */

/**
 * @author Sunny
 *
 */
public class Server extends Thread {

    public static final String ip = "localhost";
    public static int port = 3333;
    public static int BSPort = 3334;
    
    public static final int startingTime = 10000;//20 seconds
    public static final int syncTime = 10000;//20 seconds
    
    public static final String joinCmd = "startGame";
    public static final String moveCmd = "move";
    public static final String quitCmd = "quit";
    
    public static final String OKCode = "200";
    public static final String BSCode = "300";
    
    private static Maze game = null;
    private static ArrayList<Player> pList = new ArrayList<Player>();
    
    public boolean isPrimaryServer = true;//if false, it's Backup Server
    public static Player backupServer = null; //if a player is selected as Backup Server, this field should be updated
    public boolean isActive = true;
    private String IPPS = "";
    private int portPS = 0;
    
    /**
     * Constructor
     */
    public Server(boolean isPS, int mazeLength, int numOfTreasures)
    {
        //It's either the Pirmary Server or Backup Server
        isPrimaryServer = isPS;
        if (!isPrimaryServer) port = BSPort;
        
        //start the connection to wait for the incoming messages
        try {
            ServerConnection.openListeningConnection(port);                 
        } catch (Exception e) {
                // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        //create Maze game object, isPlaying is still false - game is not yet started
        game = new Maze(mazeLength,numOfTreasures);             
        
        if (isPS)
        {
            System.out.println("Server created! Maze is initialized as below:");
            game.printMaze();
        }
    }
    
    public Server(boolean isPS, int mazeLength, int numOfTreasures, int _port)
    {
        //It's either the Pirmary Server or Backup Server
        port = _port;
        isPrimaryServer = isPS;
        //if (!isPrimaryServer) port = BSPort;
        
        //start the connection to wait for the incoming messages
        try {
            ServerConnection.openListeningConnection(port);                 
        } catch (Exception e) {
                // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        //create Maze game object, isPlaying is still false - game is not yet started
        game = new Maze(mazeLength,numOfTreasures);
        if (isPS)
        {
	        System.out.println("Server created! Maze is initialized as below:");
	        game.printMaze();
        }
    }
    
    public Maze getGame()
    {
        return game;
    }
    
    /**
     * Start game
     */
    private boolean startGame()
    {
        if (game == null)
            return false;
        
        if (game.getIsPlaying())
            return false;
        
        //server should have finalized player list
        game.setPlayers(pList);         
        
        //              
        return game.startGame();
    }
    
    
    public void run()
    {
        if (isPrimaryServer)
            System.out.println("Primary Server is running...");
        
        boolean firstPlayer = true;
        boolean firstObjectReceived = true;
        
        //
        while(true)
        {
        	//System.out.println("[Server]Waiting for the incoming msg....");
            String  msg = ServerConnection.receive();
            
            //if (msg == null)
            	//continue;
            
            System.out.println("[Server]Message received: "+msg);
            System.out.print(">");
            
            if (msg.startsWith("startGame") && firstPlayer)
            {
                firstPlayer = false;
                Runnable r = new Runnable() {
                  public void run(){
                    try {
                        Thread.sleep(startingTime);
                        startGame();
                        if (backupServer != null) //sunny - to handle null pointer
                        {
                        //System.out.println("Enter here 1 " + backupServer.getIp()+ " " + (backupServer.getPort()+1));
                            startSynchronizationPS(); //Timer for synchronization starts
                            ClientConnection.openConnection(backupServer.getIp(), backupServer.getPort()+1);
                            ClientConnection.send(BSCode+"_"+ClientConnection.connectionSocket.getLocalAddress().toString().substring(1)+"_"+port);
                            //ClientConnection.closeConnection();
                        }
                        ClientConnection.openConnection(backupServer.getIp(), backupServer.getPort()+1);
                        ClientConnection.sendGameState(getGame());
                        broadcastGameMsg();//broadcast StartGame
                        System.out.println("Start the Game");                                       
                        } catch (Exception e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                        }
                  }
                };
                Thread T = new Thread(r);
                T.start();
            }
            if (msg.startsWith(BSCode))
            {
                //System.out.println("Enter here");
            	StringTokenizer st = new StringTokenizer(msg,"_");
            	if (st.hasMoreElements())
            		st.nextElement();
            	if (st.hasMoreElements())
                    IPPS = st.nextToken();
            	if (st.hasMoreElements())
                    portPS = Integer.parseInt(st.nextToken());
                game = ServerConnection.receiveGameState();
                pList = game.getPlayers();
                //System.out.println("Received game state: "+game);
                //this.pList = server.pList; not need this
                if (firstObjectReceived)
                {
                    isActive = true;
                    firstObjectReceived = false;
                    //IPPS = ClientConnection.connectionSocket.getLocalAddress().toString().substring(1);
                    //portPS = ServerConnection.serverSocket.get
                    startSynchronizationBS();
                }
                else
                {
                    isActive = true;
                    //System.out.print(">");
                }
            }
            /**
             * to inform that PS has crashed
             * it should update PS with BS's info and update BS's info from the incoming msg
             */
            if (msg.startsWith("400")) //
            {
                StringTokenizer st = new StringTokenizer(msg,"_");
                Client.serverIpAddress = Client.bsIpAddress;
                Client.serverPort = Client.bsPort;
                if (st.hasMoreElements())
                	st.nextElement();
                if (st.hasMoreElements())
                    Client.bsIpAddress = st.nextToken();
                if (st.hasMoreElements())
                    Client.bsPort = Integer.parseInt(st.nextToken());
                //System.out.println("DEBUG RECEIVE "+ Client.bsIpAddress+ " " + Client.bsPort);
            }
            /**
             * to inform that BS has crashed
             * it should update BS's info from the incoming msg
             */
            if (msg.startsWith("500"))
            {
                StringTokenizer st = new StringTokenizer(msg,"_");
                st.nextElement();
                if (st.hasMoreElements())
                    Client.bsIpAddress = st.nextToken();
                if (st.hasMoreElements())
                    Client.bsPort = Integer.parseInt(st.nextToken());
                //System.out.println("DEBUG RECEIVE "+ Client.bsIpAddress+ " " + Client.bsPort);
            }
            
            if (game.isGameOver())
            {
                //System.out.println("Game over");
            	/* no need to broadcast anymore
                if (isPrimaryServer)
                	broadcastGameMsg(); //Broadcast EndGame
                firstPlayer = true;
                */    	  	
            }
            
            new ServerSubThread(ServerConnection.connectionSocket,msg).start();
            //ServerConnection.closeConnection();
        }               
    }       
    
    /**
     * Note: Step 3: Broadcast message should contain BS's info too
     */
    private void broadcastGameMsg()
    {
        System.out.println("Broadcasting....size="+pList.size());
        for (int x=0; x < pList.size(); x++)
        {
            Player p = pList.get(x);
            String msg = OKCode+"_"+p.getIdNo()+"_"+p.getXPos()+"_"+p.getYPos()+"_"+p.getTreaCollected()
            +"_"+backupServer.getIp()+"_"+(backupServer.getPort()+1)+"_"+game.gameState();
            
            ServerConnection.send(msg,p.getSocket());
        }               
    }
    
    private void broadcastNewBS(int code) //400 = PS crash, 500 = BS crash
    {
    	String msg = code+"_"+backupServer.getIp()+"_"+(backupServer.getPort()+1);
        for (int x=0; x < pList.size(); x++)
        {
            Player p = pList.get(x);
            //String msg = "";
            //if ((p.getIp().equalsIgnoreCase(backupServer.getIp())) && ((p.getIp().equalsIgnoreCase(ip))))
                //msg = code+"_"+backupServer.getIp()+"_"+(backupServer.getPort()+1);
            
            //System.out.println("DEBUG "+backupServer.getIp()+ " "+ (backupServer.getPort()+1));
            //System.out.println("DEBUG P "+p.getIp()+ " "+ (p.getPort()+1));
            ClientConnection.openConnection(p.getIp(), p.getPort()+1);
            ClientConnection.send(msg);
        }               
    }
    
    private boolean startSynchronizationPS()
    {
        if (backupServer == null)
            return false;
        
        boolean ret = true;
        
        Runnable r2 = new Runnable() {
                  public void run(){
                    try {
                        do
                        {
                            Thread.sleep(syncTime);
                            if (ClientConnection.openConnection(backupServer.getIp(), backupServer.getPort()+1))
                            {
                                System.out.println("[Server] Can connect to BS");
                                if (!ClientConnection.send(BSCode+"_"+ClientConnection.connectionSocket.getLocalAddress().toString().substring(1)+"_"+port))
                                    System.out.println("[Server] Cannot send BSCode to BS");
                                ClientConnection.openConnection(backupServer.getIp(), backupServer.getPort()+1);
                                if (!ClientConnection.sendGameState(getGame()))
                                    System.out.println("[Server] Cannot send GameState to BS");
                                System.out.print(">");
                            }
                            else //Cannot connect to BS, BS Crash
                            {
                            	System.out.println("[Server] BS Crash");
                            	getGame().quitGame(backupServer);
                            	backupServer = Server.ServerSubThread.selectNewBS(backupServer);
                            	//getGame().getPlayers().remove(backupServer);
                                //pList.remove(backupServer);
                                if (backupServer!=null)
                                {
                                    //while (!ClientConnection.openConnection(backupServer.getIp(), backupServer.getPort()+1))
                                    	//backupServer = Server.ServerSubThread.selectNewBS(backupServer);
                                	ClientConnection.openConnection(backupServer.getIp(), backupServer.getPort()+1);
                                	ClientConnection.send(BSCode+"_"+ClientConnection.connectionSocket.getLocalAddress().toString().substring(1)+"_"+port);
                                    ClientConnection.openConnection(backupServer.getIp(), backupServer.getPort()+1);
                                    ClientConnection.sendGameState(getGame());
                                    broadcastNewBS(500);
                                }
                            }
                        }while (true);
                        } catch (Exception e) {
                                // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                  }
                };
        Thread T2 = new Thread(r2);
        T2.start();
        
        return ret;
    }
    public void startSynchronizationBS()
    {
        Runnable r = new Runnable() {
              public void run(){
                try {
                    do
                    {
                        isActive = false;
                        Thread.sleep(syncTime+10000); //waiting for object from server
                        //System.out.println("PS Status : "+isActive);
                        if (!isActive) //object didn't come, PS crash
                        {
                        	//if (!ClientConnection.openConnection(IPPS, portPS)) //confirm PS crash
                        	//{
	                            System.out.println("[Server] PS Crash");
	                            //BS become PS
	                            isPrimaryServer = true;
	                            //System.out.println("DEBUG "+IPPS+" "+(portPS-1));
	                            getGame().quitGame(searchPlayer(IPPS,portPS-1));
	                            backupServer = Server.ServerSubThread.selectNewBS(ServerConnection.connectionSocket.getLocalAddress().toString().substring(1),ServerConnection.connectionSocket.getLocalPort()-1);
	                            //getGame().getPlayers().remove(backupServer);
	                            //pList.remove(backupServer);
	                            if (backupServer!=null)
	                            {
	                                startSynchronizationPS();
	                                        
	                                //send game state to new BS
	                                //while (!ClientConnection.openConnection(backupServer.getIp(), backupServer.getPort()+1))
	                                	//backupServer = Server.ServerSubThread.selectNewBS(ServerConnection.connectionSocket.getLocalAddress().toString().substring(1),ServerConnection.connectionSocket.getLocalPort());
	                                ClientConnection.openConnection(backupServer.getIp(), backupServer.getPort()+1);
	                                ClientConnection.send(BSCode+"_"+ClientConnection.connectionSocket.getLocalAddress().toString().substring(1)+"_"+port);
	                                ClientConnection.openConnection(backupServer.getIp(), backupServer.getPort()+1);
	                                ClientConnection.sendGameState(getGame());
	                                broadcastNewBS(400);
	                            }
	                            break;
                        	//}
                        }
                    }while (true);  
                    } catch (Exception e) {
                            // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
              }
            };
        Thread T = new Thread(r);
        T.start();
    }
    
    public Player searchPlayer(String IP, int port)
    {
        Player p2 = null;
        for (int x=0; x < game.getPlayers().size(); x++)
        {
            Player p = game.getPlayers().get(x);
            if ((p.getPort() == port) && (p.getIp().equalsIgnoreCase(IP)))
            {
            	p2 = game.getPlayers().get(x);
                break;
            }
        }
        return p2;
    }
    
    /**
     * Server Thread
     */
     static class ServerSubThread extends Thread {
             
         Socket connectionSocket;
         String msg = null;
         
         public ServerSubThread(Socket soc, String clientMsg) {
                        // TODO Auto-generated constructor stub
            msg = clientMsg;
            connectionSocket = soc;
         }
             
         public void run() {
        	 String ip = connectionSocket.getInetAddress().toString().substring(1);
        	 int port = connectionSocket.getPort();
        	 
        	 if (game.isGameOver())
        	 {
        		 ServerConnection.send("Game is over!",connectionSocket);
        	 }
        	 else
        	 {
              if (msg.startsWith(joinCmd) && !game.getIsPlaying())
              {                       
                      /**
                       * Note: Step 2: Selecting BS
                       * Should check if there has been already a Backup Server.
                       * If not, consider to choose the current client to be the BS and update the BS's info for PS accordingly
                       * (choosing BS is from a method in Server)
                       */
                      
              synchronized (pList) {
                /********** Critical Section *******************/
                     Player p = createPlayer(ip,port);
                     if ((Server.backupServer == null) && (pList.size() > 0))
                     {
                             Server.backupServer = p;
                             //String msg = BSCode;
                                     //ServerConnection.send(msg,connectionSocket);
                                     //ServerConnection.sendGameState(game);
                                     //ServerConnection.sendListofPlayers(pList);
                     }
                /********** Critical Section *******************/
              }
              }
              else if (msg.startsWith(joinCmd) && game.getIsPlaying())
              {
                      String msg = "Game is started! Please try again later";
                      ServerConnection.send(msg,connectionSocket);
              }
              else if (msg.startsWith(moveCmd))
              {
                        StringTokenizer st = new StringTokenizer(msg,"_");
                        String id = null;
                        String dir = null;
                        Player p = null;
                        
                        //move command
                        if (st.hasMoreElements())
                                st.nextElement();
                        
                        //userID
                        if (st.hasMoreElements())
                        {
                                id = st.nextToken();
                                
                                //lookup for the player
                                for (int i=0;i<pList.size();i++)
                                {
                                        Player pl = pList.get(i);
                                        if (pl.getIdNo().equalsIgnoreCase(id))
                                        {
                                                p = pl;
                                                break;
                                        }
                                }                                               
                        }
                        
                        //move direction
                        if (st.hasMoreElements())
                        {
                                dir = st.nextToken();
                        }
                        
                        synchronized (game) {
                    /********** Critical Section *******************/
                        boolean ret = game.movePlayer(dir, 1, p);
                    /********** Critical Section *******************/                        
                }
                            
                    //send msg to the client
                    
                String msg = OKCode+"_"+p.getIdNo()+"_"+p.getXPos()+"_"+p.getYPos()+"_"+p.getTreaCollected()+"_"
                +"_"+game.gameState();
                //ClientConnection.openConnection(p.getIp(), p.getPort());
                //ClientConnection.send(msg);
                //ServerConnection.openConnection(p.getIp(), p.getPort());
                //ServerConnection.send(msg);
                ServerConnection.send(msg,connectionSocket);
              }
              else if (msg.startsWith(quitCmd))
              {
                      StringTokenizer st = new StringTokenizer(msg,"_");
                      Player p = null;
                      String id = null;
                      
                      //quit command
                      if (st.hasMoreElements())
                              st.nextElement();
                            
                      //userID
                      if (st.hasMoreElements())
                      {
                              id = st.nextToken();
                                    
                                    //lookup for the player
                                    for (int i=0;i<pList.size();i++)
                                    {
                                            Player pl = pList.get(i);
                                            if (pl.getIdNo().equalsIgnoreCase(id))
                                            {
                                                    p = pl;
                                                    break;
                                            }
                                    }                                               
                      }
                      
                      synchronized (game) {
                        /********** Critical Section *******************/
                            boolean ret = game.quitGame(p);
                        /********** Critical Section *******************/                        
                  }
                      
                      //send msg to the client
                      String msg = OKCode+"_Bye";                                   
                      ServerConnection.send(msg,connectionSocket);
                    
              }
        	 }
          }
          
          private Player createPlayer(String clientIP, int clientPortNo)
              {
              //check if the maximum number of players hit
                      if (pList.size() == game.getLength())
                              return null;
                            
                      SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
                      String tt = dateFormat.format(new Timestamp(System.currentTimeMillis()));
                            
                      Player p = new Player(clientIP,clientPortNo,tt,connectionSocket);
                      pList.add(p); 
                      System.out.println("a player is created!");
                            
                      return p;
              }
          
          public static Player selectNewBS(Player player)
          {
                //if (player == null) System.out.println("NULLLLLLLLLLLLLLLLLLLL");
                Player p2 = null;
                int x;
                //System.out.println("DEBUG "+player.getIdNo());
                for (x=0; x < game.getPlayers().size()-1; x++)
                {
                    Player p = game.getPlayers().get(x);
                    //System.out.println("DEBUG P "+p.getIdNo());
                    if ((p.getIdNo().equalsIgnoreCase(player.getIdNo())) && (game.getPlayers().get(x+1).getXPos()!=-1) && (game.getPlayers().get(x+1).getYPos()!=-1))
                    //if (p.getIdNo().equalsIgnoreCase(player.getIdNo()))
                    {
                    	//System.out.println("Enter here");
                            p2 = game.getPlayers().get(x+1);
                            break;
                    }
                }
                if (p2==null) p2 = game.getPlayers().get(game.getPlayers().size()-1);
                return p2;
          }
          
          public static Player selectNewBS(String IP, int port)
          {
                //if (player == null) System.out.println("NULLLLLLLLLLLLLLLLLLLL");
                Player p2 = null;
                //System.out.println("DEBUG "+IP + " "+port);
                for (int x=0; x < game.getPlayers().size()-1; x++)
                {
                    Player p = game.getPlayers().get(x);
                    //System.out.println("DEBUG P "+p.getIp()+ " "+p.getPort());
                    if ((p.getPort() == port) && (p.getIp().equalsIgnoreCase(IP)) && (game.getPlayers().get(x+1).getXPos()!=-1) && (game.getPlayers().get(x+1).getYPos()!=-1))
                    //if ((p.getPort() == port) && (p.getIp() == IP))
                    //if (p.getPort() == port)
                    {
                    	//System.out.println("Enter here");
                    	p2 = game.getPlayers().get(x+1);
                        break;
                    }
                }
                if (p2==null)
                	p2 = game.getPlayers().get(game.getPlayers().size()-1);
                return p2;
          }
        
       }
     
     /**
      * Note: Step 1: Bootstrap method
      * - To start the Primary Server
      * - Run the Client and make the 1st request, therefore timer is started to count 20s
      * - This method will be called in main()
      */
     
     /**
      * Note: Step 2: Startup Backup Server method
      * When it is selected as a Backup Server, it is requested to start as a Backup Server
      * (eg isPrimaryServer = false)
      * - This method is started when it is requested to do so (via received msg)
      */
     
     /**
      * Note: Step 4: Communication between BS and PS
      * Each server has its own timer to reset the flag whether the other is Active
      * PS based on timer to send game state to BS
      * If PS die, BS become PS and look for new BS
      * If BS die, PS looks for new BS
      */
     
     /**
      * Method to look for BS
      */
     
     public static String readCommand(){
                    String msg = "";
                    
                    BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in));
                    try {
                            msg = userInput.readLine();
                    } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                    }
                    
                    return msg;
    }
     
    /**
     * @param args
     */
    public static void main(String[] args) {
            // TODO Auto-generated method stub
            /**
             * Note: should get input from users, which are size of maze and number of treasures
             * and input them as constructor inputs of the Server class
             */
            
            int length = 0;
            int numTreasures = 0;
            try {
                    System.out.println("Preparing to Bootstrap Server....");
                    System.out.print("Maze size = ");
                    length = Integer.parseInt(readCommand());
                    System.out.print("Number of treasures = ");
                    numTreasures = Integer.parseInt(readCommand());
                    
                    if (length > 0 && numTreasures > 0)
                    {
                            System.out.println("Starting the server...");                           
                            Server server = new Server(true,length,numTreasures);
                            server.start();
                            
                            /*
                            //Start the client thread
                            Class<? extends Runnable> theClass = 
                                Class.forName("Client").asSubclass(Runnable.class);
                            Runnable instance = theClass.newInstance();
                            new Thread(instance).start();
                            */
                    }
                    else
                            System.out.println("Invalid input to create a maze!");
            }
            catch (Exception e)
            {
                    System.out.println("Invalid input! Please try again.");
                    e.printStackTrace();
            }
            //Client client = new Client();
            //client.run();
    }
}